/* 
 * Copyright (C) 2011 rccloud.com All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.rccloud.inproxy;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.rccloud.inproxy.annotation.AutoProxy;
import com.rccloud.inproxy.annotation.NestedProxy;
import com.rccloud.inproxy.annotation.NoProxy;
import com.rccloud.inproxy.annotation.ProxyField;
import com.rccloud.inproxy.converter.DefaultProxyConverter;
import com.rccloud.inproxy.converter.ProxyFieldConverter;
import com.rccloud.inproxy.utils.ReflectionUtils;

/**
 * Resolver factory for resolve proxy info and cache it.
 */
public class InproxyFactory {

	private static InproxyFactory factory;

	/**
	 * Get the factory
	 * 
	 * @return Factory
	 */
	public static InproxyFactory getFactory() {
		try {
			return getFactory(new InproxyConfigImpl());
		} catch (IOException e) {
			throw new InproxyFactoryInitException(
					"Can't find default config file.");
		}
	}

	/**
	 * Get the factory
	 * 
	 * @param config
	 *            Config object
	 * @return Return inproxy factory
	 */
	public static InproxyFactory getFactory(InproxyConfig config) {
		if (factory == null)
			factory = new InproxyFactory(config);
		return factory;
	}

	/**
	 * Mapping info from main object class to proxy object classes.
	 * 
	 * <pre>
	 * 	Main class -> [proxy class1 -> proxy info]
	 *                [proxy class2 -> proxy info] 
	 *                ...
	 * </pre>
	 */
	private Map<Class<?>, Map<Class<?>, InproxyInfo>> mainMap;

	/**
	 * Mapping info from proxy object class to main object class.
	 * 
	 * <pre>
	 *  Proxy class -> proxy info
	 * </pre>
	 */
	private Map<Class<?>, InproxyInfo> proxiesMap;

	private InproxyFactory(InproxyConfig config) {
		mainMap = new HashMap<Class<?>, Map<Class<?>, InproxyInfo>>();
		proxiesMap = new HashMap<Class<?>, InproxyInfo>();
		resolveMapping(config.getAnnotatedClasses());
	}

	/**
	 * Find mapping proxy info from main class
	 * 
	 * @param mainClass
	 *            Main class
	 * @return Return proxy info only if only one proxy class found.
	 */
	public InproxyInfo getProxyInfoFromMainClass(Class<?> mainClass) {
		Map<Class<?>, InproxyInfo> map = mainMap.get(mainClass);
		if (map != null && map.values().size() > 0) {
			if (map.values().size() == 1) {
				return map.values().iterator().next();
			} else {
				throw new ProxyConverterException(
						"Found multiple proxy classes, you should pass the excepted proxy class as parameter.");
			}
		}
		return null;
	}

	/**
	 * Find mapping proxy info from main class
	 * 
	 * @param mainClass
	 *            Main class
	 * @param expectedProxyClass
	 *            Excepted proxy class
	 * @return Return proxy info
	 */
	public InproxyInfo getProxyInfoFromMainClass(Class<?> mainClass,
			Class<?> expectedProxyClass) {
		Map<Class<?>, InproxyInfo> map = mainMap.get(mainClass);
		if (map != null) {
			if (map.containsKey(expectedProxyClass))
				return map.get(expectedProxyClass);
			else {
				// Try to search proxy sub class
				Collection<InproxyInfo> proxyClasses = map.values();
				for (InproxyInfo proxyInfo : proxyClasses) {
					if (expectedProxyClass.isAssignableFrom(proxyInfo
							.getProxyClass())) {
						return proxyInfo;
					}
				}
			}
		}
		return null;
	}

	/**
	 * Find mapping proxy info from proxy class
	 * 
	 * @param proxyClass
	 *            Proxy class
	 * @return Return proxy info
	 */
	public InproxyInfo getProxyInfoFromProxyClass(Class<?> proxyClass) {
		return proxiesMap.get(proxyClass);
	}

	/**
	 * Resolve mapping info
	 * 
	 * @param classes
	 *            Class need to be resolved
	 */
	private void resolveMapping(List<Class<?>> proxyClasses) {
		for (Class<?> proxyClass : proxyClasses) {
			InproxyInfo proxyInfo = resolveProxyInfo(proxyClass);
			Class<?> mainClass = getMainClass(proxyClass);
			proxiesMap.put(proxyClass, proxyInfo);
			if (mainMap.containsKey(mainClass)) {
				mainMap.get(mainClass).put(proxyClass, proxyInfo);
			} else {
				Map<Class<?>, InproxyInfo> map = new HashMap<Class<?>, InproxyInfo>();
				map.put(proxyClass, proxyInfo);
				mainMap.put(mainClass, map);
			}

		}
	}

	/**
	 * Get main class type from proxy class type
	 * 
	 * @param proxyClass
	 *            Proxy class type
	 * @return Return main class type
	 */
	private Class<?> getMainClass(Class<?> proxyClass) {
		if (proxyClass.isAnnotationPresent(AutoProxy.class)) {
			return proxyClass.getAnnotation(AutoProxy.class).value();
		} else {
			throw new ProxyConverterException(
					"Proxy class must be annotated with 'AutoProxy'");
		}
	}

	/**
	 * Generate proxy info for proxy class
	 * 
	 * @param proxyClass
	 *            Proxy class
	 * @return Return proxy info
	 */
	private InproxyInfo resolveProxyInfo(Class<?> proxyClass) {
		Map<Field, FieldProxyInfo> fieldMap = scanFieldsProxyInfo(proxyClass);
		List<MethodProxyInfo> methodList = scanMethodProxyInfo(proxyClass,
				fieldMap);
		InproxyInfo info = new InproxyInfo();
		info.setProxyClass(proxyClass);
		info.setMainClass(getMainClass(proxyClass));
		info.setMethodList(methodList);
		return info;
	}

	/**
	 * Scan field info
	 * 
	 * @param proxyClass
	 *            Proxy class type
	 * @return Return field mapping info
	 */
	private Map<Field, FieldProxyInfo> scanFieldsProxyInfo(Class<?> proxyClass) {
		Map<Field, FieldProxyInfo> fieldMap = new HashMap<Field, FieldProxyInfo>();

		Class<?> mainClass = proxyClass.getAnnotation(AutoProxy.class).value();

		Field[] fields = ReflectionUtils.getAllFields(proxyClass, Object.class);
		for (Field field : fields) {

			// Generate field proxy info

			FieldProxyInfo proxyInfo = new FieldProxyInfo();
			proxyInfo.setProxyField(field);
			proxyInfo.setTargetClass(mainClass);
			proxyInfo.setTargetFieldName(field.getName());
			proxyInfo.setConverter(DefaultProxyConverter.class);

			if (field.isAnnotationPresent(ProxyField.class)) {
				ProxyField proxyField = field.getAnnotation(ProxyField.class);
				if (!proxyField.name().trim().equals(""))
					proxyInfo.setTargetFieldName(proxyField.name());
				proxyInfo.setConverter(proxyField.converter());
			}
			if (field.isAnnotationPresent(NestedProxy.class)) {
				proxyInfo.setConverter(ProxyFieldConverter.class);
			}
			if (field.isAnnotationPresent(NoProxy.class)) {
				proxyInfo = null;
			}
			if (proxyInfo != null)
				fieldMap.put(field, proxyInfo);
		}
		return fieldMap;
	}

	/**
	 * Scan method info based on field mapping info
	 * 
	 * @param proxyClass
	 *            Proxy class type
	 * @param fieldMap
	 *            Field mapping info
	 * @return Return method info
	 */
	private List<MethodProxyInfo> scanMethodProxyInfo(Class<?> proxyClass,
			Map<Field, FieldProxyInfo> fieldMap) {
		Set<Field> fields = fieldMap.keySet();
		List<MethodProxyInfo> methodList = new ArrayList<MethodProxyInfo>();
		for (Field field : fields) {
			FieldProxyInfo fieldProxy = fieldMap.get(field);

			Method setMethod = ReflectionUtils.getSetMethod(proxyClass, field);
			Method getMethod = ReflectionUtils.getGetMethod(proxyClass, field);
			Field targetField = ReflectionUtils.getField(
					fieldProxy.getTargetClass(),
					fieldProxy.getTargetFieldName(), field.getName());
			Method targetGetMethod = ReflectionUtils.getGetMethod(
					fieldProxy.getTargetClass(), targetField);
			Method targetSetMethod = ReflectionUtils.getSetMethod(
					fieldProxy.getTargetClass(), targetField);

			MethodProxyInfo methodProxy = new MethodProxyInfo();
			methodProxy.setProxyField(field);
			methodProxy.setSetMethod(setMethod);
			methodProxy.setGetMethod(getMethod);
			methodProxy.setTargetClass(fieldProxy.getTargetClass());
			methodProxy.setTargetGetMethod(targetGetMethod);
			methodProxy.setTargetSetMethod(targetSetMethod);
			methodProxy.setConverter(fieldProxy.getConverter());

			methodList.add(methodProxy);
		}
		return methodList;
	}
}
